# Moths - an environment for mathy calculations
# Copyright (C) 2006 Mike Sutjipto
# Licensed under the GPL

module Moths
  
  # The parser. Takes a tokenizer and constructs the AST.
  #
  class Parser
    
    # The entry point for the parser.  Takes a tokenizer and an environment - 
    # the latter of which is necessary for definitions and identifiers
    #
    def parse(tokenizer, env)
      return expr if !tokenizer.next?

      t = tokenizer.next
      case t.token_type
        when :definition
          return self.parse_def(tokenizer, env)

        when :ed
          raise MothsError, 'edit not implemented yet!'

        else
          tokenizer.put_back
          return self.parse_expr(tokenizer)
      end
      expr = self.parse_expr(tokenizer)
    end
    
    # Parses an expression. Generally this calls the other, more specific
    # parsing functions
    #
    def parse_expr(tokenizer, expr = nil)
      return expr if !tokenizer.next?

      t = tokenizer.next
      case t.token_type
        when :openParens
          parens = Parentheses.new(self.parse(tokenizer, nil))
          return self.parse_expr(tokenizer, parens)

        when :closeParens
          return expr

        when :realNumber
          # this prevents stray numbers
          if expr != nil then
            raise MothsError, 'Unexpected number: ' + t.contents
          end

          num = Number.new(:realNumber, t.contents.to_f)
          return self.parse_expr(tokenizer, num)

        when :integer
          # this prevents stray numbers
          if expr != nil then
            raise MothsError, 'Unexpected number: ' + t.contents
          end

          num = Number.new(:integer, t.contents.to_i)
          return self.parse_expr(tokenizer, num)

        when :identifier
          id = self.parse_identifier(tokenizer, Identifier.new(t.contents, []))
          return self.parse_expr(tokenizer, id)

        when :plus, :minus, :times, :div, :exp
          op = self.parse_op(tokenizer, expr, t.token_type)
          return self.parse_expr(tokenizer, op)

        else
          raise MothsError, 'Unexpected symbol in expression: ' + t.tokenType.to_s
      end
    end

    # Parses an operation, the left side of which has already been parsed
    # and passed into 'left'
    #
    def parse_op(tokenizer, left, opType)
      # check for errors
      raise MothsError, 'no left side in op' if left == nil
      raise MothsError, 'no right side in op' if !tokenizer.next?

      # at this point, we need to get the right side for the op
      t = tokenizer.next
      case t.token_type
        when :openParens
          parens = Parentheses.new(self.parseExpr(tokenizer))
          expr = Operation.new(opType, left, parens)

        when :closeParens
          raise MothsError, 'Unexpected symbol: closeParens'

        when :realNumber
          num = Number.new(:realNumber, t.contents.to_f)
          expr = Operation.new(opType, left, num)

        when :integer
          num = Number.new(:integer, t.contents.to_i)
          expr = Operation.new(opType, left, num)

        when :identifier
          id = self.parse_identifier(tokenizer, Identifier.new(t.contents, []))
          expr = Operation.new(opType, left, id)

        when :plus, :minus, :times, :div, :exp
          raise MothsError, 'Unexpected symbol: ' + t.token_type.to_s

        else
          raise MothsError, 'Unexpected symbol in operation: ' + t.token_type.to_s
      end

      return self.pemdas(expr)
    end

    # Parses an identifier, mostly just grabbing the arguments (which are
    # expressions)
    #
    def parse_identifier(tokenizer, id)
      return id if !tokenizer.next?

      t = tokenizer.next
      case t.token_type
        when :openParens
          parens = Parentheses.new(self.parse_expr(tokenizer))
          id.add_arg(parens)
          return self.parse_identifier(tokenizer, id)

        when :integer
          id.add_arg(Number.new(t.token_type, t.contents.to_i))
          return self.parse_identifier(tokenizer, id)

        when :realNumber
          id.add_arg(Number.new(t.token_type, t.contents.to_f))
          return self.parse_identifier(tokenizer, id)

        when :identifier
          # we're greedy here and assume this id takes no arguments
          id.add_arg(Identifier.new(t.contents, []))
          return parse_identifier(tokenizer, id)

        else
          tokenizer.put_back
          return id
      end
    end

    # Parses a definition and adds to the current environment
    #
    def parse_def(tokenizer, env)
      id = ''
      arg_list = []
      expr = nil

      # first grab the id
      t = tokenizer.next
      case t.token_type
        when :identifier
          id = t.contents

        else
          raise MothsError, 'Expecting function identifier!'
      end

      # grab the arguments
      while t = tokenizer.next
        case t.token_type
          when :identifier
            arg_list = arg_list << t.contents

          when :equals
            break

          else
            raise MothsError, 'Unexpected symbol in argument def: ' + t.token_type.to_s
        end
      end

      expr = self.parse_expr(tokenizer)
      env.define(Definition.new(id, arg_list, expr))

      return Number.new(:integer, 1)
    end

    # Rotate nodes in expr to maintain PEMDAS ordering
    #
    def pemdas(expr)
      if expr.precedence > expr.left_child.precedence then
        # perform the rotation and continue PEMDASing
        new_expr = expr.left_child
        expr.left_child = expr.left_child.right_child
        new_expr.right_child = self.pemdas(expr)

        return new_expr
      end

      return expr
    end
  end
  
end
